package com.black.utils.file.compress.zip;

import com.black.utils.file.FileTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import static com.black.utils.file.FileUtil.BUFFER_SIZE;

/**
 * @author ylx
 */
@Slf4j
public class ZipUtil {

    /**
     * 将指定路径的文件压缩成zip文件
     * 根据源文件名，自动生成压缩包名
     * 根据containsSelf，是否包含目录本身
     *
     * @param srcPath      源文件路径
     * @param containsSelf 是否包含目录本身
     * @throws IOException 运行时IO异常
     */
    public static void toZip(String srcPath, boolean containsSelf) throws IOException {
        toZip(new File(srcPath), containsSelf);
    }

    public static void toZip(File src, boolean containsSelf) throws IOException {
        if (!src.exists()) {
            throw new IOException("src not exists");
        }
        String absolutePath = src.getAbsolutePath();
        absolutePath = FileTypeEnum.transferExtension(absolutePath, FileTypeEnum.ZIP);
        File zipFile = new File(absolutePath);
        zipFile.createNewFile();
        if (src.isDirectory() && !containsSelf) {
            toZip(zipFile, src.listFiles());
        } else {
            toZip(zipFile, src);
        }
    }

    public static void toZip(File dist, File... files) throws IOException {
        List<File> srcFiles = Arrays.asList(files);
        toZip(dist, srcFiles);
    }

    public static void toZip(File dist, String... files) throws IOException {
        List<File> srcFiles = Arrays.stream(files).map(File::new).collect(Collectors.toList());
        toZip(dist, srcFiles);
    }

    public static void toZip(String dist, File... files) throws IOException {
        toZip(new File(dist), files);
    }

    public static void toZip(String dist, String... files) throws IOException {
        toZip(new File(dist), files);
    }

    /**
     * Files to zip (recursively handle sub folders).
     *
     * @param dist     输出文件
     * @param srcFiles 源文件列表（可以包含文件或文件夹）
     * @throws IOException 运行异常
     */
    public static void toZip(File dist, List<File> srcFiles) throws IOException {
        if (CollectionUtils.isEmpty(srcFiles)) {
            throw new IOException("no files to zip");
        }
        try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(dist.toPath()))) {
            for (File srcFile : srcFiles) {
                compress(srcFile, zos, "");
            }
        }
    }

    /**
     * 递归压缩文件或文件夹
     *
     * @param file     要压缩的文件或文件夹
     * @param zos      ZipOutputStream
     * @param basePath 相对路径（用于保留压缩包内的文件夹结构）
     * @throws IOException IO异常
     */
    private static void compress(File file, ZipOutputStream zos, String basePath) throws IOException {
        // 如果是目录，递归处理
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            if (files == null || files.length == 0) {
                // 空文件夹也需要创建一个ZipEntry
                zos.putNextEntry(new ZipEntry(basePath + file.getName() + FileTypeEnum.PATH_SEPARATOR));
                zos.closeEntry();
            } else {
                // 递归压缩子文件和子文件夹
                for (File f : files) {
                    compress(f, zos, basePath + file.getName() + FileTypeEnum.PATH_SEPARATOR);
                }
            }
        } else {
            // 压缩单个文件
            byte[] buf = new byte[BUFFER_SIZE];
            zos.putNextEntry(new ZipEntry(basePath + file.getName()));
            try (FileInputStream in = new FileInputStream(file)) {
                int len;
                while ((len = in.read(buf)) != -1) {
                    zos.write(buf, 0, len);
                }
            }
            zos.closeEntry();
        }
    }

    /**
     * Unzip a zip file to the same directory
     * 解压到当前目录
     *
     * @param zipFile zip压缩文件
     * @throws IOException 运行时IO异常
     */
    public static void unZip(String zipFile) throws IOException {
        unZip(new File(zipFile));
    }

    /**
     * 解压到与当前压缩包所在目录
     *
     * @param zipFile zip压缩文件
     * @throws IOException 运行时IO异常
     */
    public static void unZip(File zipFile) throws IOException {
        unZip(zipFile, true);
    }

    public static void unZip(File zipFile, boolean isCreate) throws IOException {
        File dist = zipFile.getParentFile();
        if (isCreate) {
            dist = new File(zipFile.getParentFile(), FileTypeEnum.getPrefix(zipFile));
            dist.mkdirs();
        }
        unZipTo(zipFile, dist);
    }

    public static void unZipTo(String zipFile, String dist) throws IOException {
        unZipTo(new File(zipFile), new File(dist));
    }

    public static void unZipTo(File zipFile, String dist) throws IOException {
        unZipTo(zipFile, new File(dist));
    }

    public static void unZipTo(String zipFile, File dist) throws IOException {
        unZipTo(new File(zipFile), dist);
    }

    /**
     * Unzip a zip file to a specified directory
     * 解压到指定目录
     *
     * @param zipFile zip压缩文件
     * @param dist    解压目标目录
     * @throws IOException 运行时IO异常
     */
    public static void unZipTo(File zipFile, File dist) throws IOException {
        // 创建解压目标目录
        if (!dist.exists() || !dist.isDirectory()) {
            dist.mkdirs();
        }
        // 验证压缩包文件类型
        if (!FileTypeEnum.ZIP.is(zipFile)) {
            throw new IOException("File type is not zip");
        }
        // 开始解压
        try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(zipFile.toPath()))) {
            ZipEntry zipEntry = zis.getNextEntry();
            while (zipEntry != null) {
                File newFile = newFile(dist, zipEntry);
                if (zipEntry.isDirectory()) {
                    if (!newFile.isDirectory() && !newFile.mkdirs()) {
                        throw new IOException("Failed to create directory " + newFile);
                    }
                } else {
                    // Ensure parent directories exist
                    File parent = newFile.getParentFile();
                    if (!parent.isDirectory() && !parent.mkdirs()) {
                        throw new IOException("Failed to create directory " + parent);
                    }
                    // Write file content
                    try (FileOutputStream fos = new FileOutputStream(newFile)) {
                        byte[] buffer = new byte[BUFFER_SIZE];
                        int len;
                        while ((len = zis.read(buffer)) > 0) {
                            fos.write(buffer, 0, len);
                        }
                    }
                }
                zis.closeEntry();
                zipEntry = zis.getNextEntry();
            }
        }
    }

    public static void batchUnZip(String srcPath, String targetPath) throws IOException {
        batchUnZip(new File(srcPath), new File(targetPath));
    }

    public static void batchUnZip(File srcDir, File targetDir) throws IOException {
        File[] files = srcDir.listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                batchUnZip(file, new File(targetDir, file.getName()));
            }
            if (FileTypeEnum.ZIP.is(file)) {
                unZipTo(file, targetDir);
            }
        }
    }

    /**
     * Utility to safely create a new File
     *
     * @param destinationDir 解压目录
     * @param zipEntry       压缩条目
     * @return 新文件对象
     * @throws IOException IO异常
     */
    private static File newFile(File destinationDir, ZipEntry zipEntry) throws IOException {
        File destFile = new File(destinationDir, zipEntry.getName());
        String destDirPath = destinationDir.getCanonicalPath();
        String destFilePath = destFile.getCanonicalPath();
        // 防止Zip Slip漏洞
        if (!destFilePath.startsWith(destDirPath + File.separator)) {
            throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
        }
        return destFile;
    }
}
